// SPDX-FileCopyrightText: 2023 Unity Technologies and the glTFast authors
// SPDX-License-Identifier: Apache-2.0

using System;

namespace GLTFast.Schema
{

    /// <summary>
    /// The topology type of primitives to render
    /// </summary>
    /// <seealso href="https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_mode"/>
    public enum DrawMode
    {
        /// <summary>Points</summary>
        Points = 0,
        /// <summary>Lines</summary>
        Lines = 1,
        /// <summary>Line loop</summary>
        LineLoop = 2,
        /// <summary>Line strip</summary>
        LineStrip = 3,
        /// <summary>Triangles</summary>
        Triangles = 4,
        /// <summary>Triangle strip</summary>
        TriangleStrip = 5,
        /// <summary>Triangle fan</summary>
        TriangleFan = 6
    }

    /// <inheritdoc />
    [Serializable]
    public class MeshPrimitive : MeshPrimitiveBase<MeshPrimitiveExtensions> { }

    /// <inheritdoc />
    /// <typeparam name="TExtensions">Mesh primitive extensions type</typeparam>
    [Serializable]
    public class MeshPrimitiveBase<TExtensions> : MeshPrimitiveBase
    where TExtensions : MeshPrimitiveExtensions
    {
        /// <inheritdoc cref="Extensions"/>
        public TExtensions extensions;

        /// <inheritdoc />
        public override MeshPrimitiveExtensions Extensions => extensions;

        /// <inheritdoc />
        internal override void UnsetExtensions()
        {
            extensions = null;
        }
    }

    /// <summary>
    /// Geometry to be rendered with the given material.
    /// </summary>
    [Serializable]
    public abstract class MeshPrimitiveBase : ICloneable, IMaterialsVariantsSlot
    {

        /// <summary>
        /// A dictionary object, where each key corresponds to mesh attribute semantic
        /// and each value is the index of the accessor containing attribute's data.
        /// </summary>
        public Attributes attributes;

        /// <summary>
        /// The index of the accessor that contains mesh indices.
        /// When this is not defined, the primitives should be rendered without indices
        /// using `drawArrays()`. When defined, the accessor must contain indices:
        /// the `bufferView` referenced by the accessor must have a `target` equal
        /// to 34963 (ELEMENT_ARRAY_BUFFER); a `byteStride` that is tightly packed,
        /// i.e., 0 or the byte size of `componentType` in bytes;
        /// `componentType` must be 5121 (UNSIGNED_BYTE), 5123 (UNSIGNED_SHORT)
        /// or 5125 (UNSIGNED_INT), the latter is only allowed
        /// when `OES_element_index_uint` extension is used; `type` must be `\"SCALAR\"`.
        /// </summary>
        public int indices = -1;

        /// <summary>
        /// The index of the material to apply to this primitive when rendering.
        /// </summary>
        public int material = -1;

        /// <summary>
        /// The type of primitives to render. All valid values correspond to WebGL enums.
        /// </summary>
        public DrawMode mode = DrawMode.Triangles;

        /// <summary>
        /// An array of Morph Targets, each  Morph Target is a dictionary mapping
        /// attributes to their deviations
        /// in the Morph Target (index of the accessor containing the attribute
        /// displacements' data).
        /// </summary>
        public MorphTarget[] targets;

        /// <inheritdoc cref="MeshPrimitiveExtensions"/>
        public abstract MeshPrimitiveExtensions Extensions { get; }

        /// <inheritdoc />
        public int GetMaterialIndex(int variantIndex)
        {
            var mapping = Extensions?.KHR_materials_variants;
            if (mapping != null && mapping.TryGetMaterialIndex(variantIndex, out var materialIndex))
            {
                return materialIndex;
            }
            return material;
        }

        /// <summary>`
        /// Sets <see cref="Extensions"/> to null.
        /// </summary>
        internal abstract void UnsetExtensions();

#if DRACO_IS_INSTALLED
        public bool IsDracoCompressed => Extensions!=null && Extensions.KHR_draco_mesh_compression != null;
#endif

        /// <summary>
        /// Determines whether two object instances are equal.
        /// </summary>
        /// <param name="obj">The object to compare with the current object.</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        // TODO: Remove upon next major release. This serves no purpose anymore except keeping the API intact.
        public override bool Equals(object obj)
        {
            // ReSharper disable once BaseObjectEqualsIsObjectEquals
            return base.Equals(obj);
        }

        /// <summary>
        /// Default hash function.
        /// </summary>
        /// <returns>A hash code for the current object.</returns>
        // TODO: Remove upon next major release. This serves no purpose anymore except keeping the API intact.
        public override int GetHashCode()
        {
            // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode
            return base.GetHashCode();
        }

        /// <summary>
        /// Clones the object
        /// </summary>
        /// <returns>Member-wise clone</returns>
        public object Clone()
        {
            return MemberwiseClone();
        }

        internal void GltfSerialize(JsonWriter writer)
        {
            writer.AddObject();
            if (attributes != null)
            {
                writer.AddProperty("attributes");
                attributes.GltfSerialize(writer);
            }
            if (indices >= 0)
            {
                writer.AddProperty("indices", indices);
            }
            if (material >= 0)
            {
                writer.AddProperty("material", material);
            }
            if (mode != DrawMode.Triangles)
            {
                writer.AddProperty("mode", (int)mode);
            }
            if (targets != null)
            {
                writer.AddArray("targets");
                foreach (var target in targets)
                {
                    target.GltfSerialize(writer);
                }
                writer.CloseArray();
            }
            if (Extensions != null)
            {
                writer.AddProperty("extensions");
                Extensions.GltfSerialize(writer);
            }
            writer.Close();
        }
    }

    /// <summary>
    /// Mesh vertex attribute collection. Each property value is the index of
    /// the accessor containing attribute’s data.
    /// </summary>
    [Serializable]
    public class Attributes
    {

        // Names are identical to glTF specified property names, that's why
        // inconsistent names are ignored.
        // ReSharper disable InconsistentNaming

        /// <summary>Vertex position accessor index.</summary>
        public int POSITION = -1;
        /// <summary>Vertex normals accessor index.</summary>
        public int NORMAL = -1;
        /// <summary>Vertex tangents accessor index.</summary>
        public int TANGENT = -1;
        /// <summary>Texture coordinates accessor index.</summary>
        public int TEXCOORD_0 = -1;
        /// <summary>Texture coordinates accessor index (second UV set).</summary>
        public int TEXCOORD_1 = -1;
        /// <summary>Texture coordinates accessor index (third UV set).</summary>
        public int TEXCOORD_2 = -1;
        /// <summary>Texture coordinates accessor index (fourth UV set).</summary>
        public int TEXCOORD_3 = -1;
        /// <summary>Texture coordinates accessor index (fifth UV set).</summary>
        public int TEXCOORD_4 = -1;
        /// <summary>Texture coordinates accessor index (sixth UV set).</summary>
        public int TEXCOORD_5 = -1;
        /// <summary>Texture coordinates accessor index (seventh UV set).</summary>
        public int TEXCOORD_6 = -1;
        /// <summary>Texture coordinates accessor index (eighth UV set).</summary>
        public int TEXCOORD_7 = -1;
        /// <summary>Texture coordinates accessor index (ninth UV set).</summary>
        public int TEXCOORD_8 = -1;
        /// <summary>Vertex color accessor index.</summary>
        public int COLOR_0 = -1;
        /// <summary>Bone joints accessor index.</summary>
        public int JOINTS_0 = -1;
        /// <summary>Bone weights accessor index.</summary>
        public int WEIGHTS_0 = -1;

        // ReSharper restore InconsistentNaming

        /// <summary>
        /// Determines whether two object instances are equal.
        /// </summary>
        /// <param name="obj">The object to compare with the current object.</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        // TODO: Remove upon next major release. This serves no purpose anymore except keeping the API intact.
        public override bool Equals(object obj)
        {
            // ReSharper disable once BaseObjectEqualsIsObjectEquals
            return base.Equals(obj);
        }

        /// <summary>
        /// Default hash function.
        /// </summary>
        /// <returns>A hash code for the current object.</returns>
        // TODO: Remove upon next major release. This serves no purpose anymore except keeping the API intact.
        public override int GetHashCode()
        {
            // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode
            return base.GetHashCode();
        }

        /// <summary>
        /// Calculates the texture coordinate set quantity.
        /// </summary>
        /// <returns>Texture coordinate set quantity.</returns>
        public int GetTexCoordsCount()
        {
            if (TEXCOORD_0 < 0) return 0;
            if (TEXCOORD_1 < 0) return 1;
            if (TEXCOORD_2 < 0) return 2;
            if (TEXCOORD_3 < 0) return 3;
            if (TEXCOORD_4 < 0) return 4;
            if (TEXCOORD_5 < 0) return 5;
            if (TEXCOORD_6 < 0) return 6;
            if (TEXCOORD_7 < 0) return 7;
            return TEXCOORD_8 < 0 ? 8 : 9;
        }

        /// <summary>
        /// Consolidates all `TEXCOORD_*` accessor fields into a single array.
        /// The result is delimited to the maximum count of texture coordinate sets Unity supports.
        /// </summary>
        /// <param name="uvAccessors">Resulting array of accessor indices.</param>
        /// <param name="limitExceeded">If true, the attributes has more UV sets than Unity supports
        /// and uvAccessors is delimited.</param>
        /// <returns>True if there's one or more UV sets and the result is valid. False otherwise.</returns>
        public bool TryGetAllUVAccessors(out int[] uvAccessors, out bool limitExceeded)
        {
            var uvCount = GetTexCoordsCount();
            if (uvCount < 1)
            {
                uvAccessors = null;
                limitExceeded = false;
                return false;
            }

            limitExceeded = uvCount > VertexBufferGeneratorBase.maxUvSetCount;
            if (limitExceeded)
            {
                uvCount = VertexBufferGeneratorBase.maxUvSetCount;
            }

            uvAccessors = new int[uvCount];
            uvAccessors[0] = TEXCOORD_0;
            if (uvAccessors.Length >= 2)
            {
                uvAccessors[1] = TEXCOORD_1;
            }
            if (uvAccessors.Length >= 3)
            {
                uvAccessors[2] = TEXCOORD_2;
            }
            if (uvAccessors.Length >= 4)
            {
                uvAccessors[3] = TEXCOORD_3;
            }
            if (uvAccessors.Length >= 5)
            {
                uvAccessors[4] = TEXCOORD_4;
            }
            if (uvAccessors.Length >= 6)
            {
                uvAccessors[5] = TEXCOORD_5;
            }
            if (uvAccessors.Length >= 7)
            {
                uvAccessors[6] = TEXCOORD_6;
            }
            if (uvAccessors.Length >= 8)
            {
                uvAccessors[7] = TEXCOORD_7;
            }
            return true;
        }

        internal void GltfSerialize(JsonWriter writer)
        {
            writer.AddObject();
            if (POSITION >= 0) writer.AddProperty("POSITION", POSITION);
            if (NORMAL >= 0) writer.AddProperty("NORMAL", NORMAL);
            if (TANGENT >= 0) writer.AddProperty("TANGENT", TANGENT);
            if (TEXCOORD_0 >= 0) writer.AddProperty("TEXCOORD_0", TEXCOORD_0);
            if (TEXCOORD_1 >= 0) writer.AddProperty("TEXCOORD_1", TEXCOORD_1);
            if (TEXCOORD_2 >= 0) writer.AddProperty("TEXCOORD_2", TEXCOORD_2);
            if (TEXCOORD_3 >= 0) writer.AddProperty("TEXCOORD_3", TEXCOORD_3);
            if (TEXCOORD_4 >= 0) writer.AddProperty("TEXCOORD_4", TEXCOORD_4);
            if (TEXCOORD_5 >= 0) writer.AddProperty("TEXCOORD_5", TEXCOORD_5);
            if (TEXCOORD_6 >= 0) writer.AddProperty("TEXCOORD_6", TEXCOORD_6);
            if (TEXCOORD_7 >= 0) writer.AddProperty("TEXCOORD_7", TEXCOORD_7);
            if (COLOR_0 >= 0) writer.AddProperty("COLOR_0", COLOR_0);
            if (JOINTS_0 >= 0) writer.AddProperty("JOINTS_0", JOINTS_0);
            if (WEIGHTS_0 >= 0) writer.AddProperty("WEIGHTS_0", WEIGHTS_0);
            writer.Close();
        }
    }

    /// <summary>
    /// Mesh primitive extensions
    /// </summary>
    [Serializable]
    public class MeshPrimitiveExtensions
    {
#if DRACO_IS_INSTALLED
        // ReSharper disable once InconsistentNaming
        public MeshPrimitiveDracoExtension KHR_draco_mesh_compression;
#endif

        /// <inheritdoc cref="MaterialsVariantsMeshPrimitiveExtension"/>
        // ReSharper disable once InconsistentNaming
        public MaterialsVariantsMeshPrimitiveExtension KHR_materials_variants;

        internal void GltfSerialize(JsonWriter writer)
        {
            writer.AddObject();
#if DRACO_IS_INSTALLED
            if (KHR_draco_mesh_compression != null) {
                writer.AddProperty("KHR_draco_mesh_compression");
                KHR_draco_mesh_compression.GltfSerialize(writer);
            }
#endif
            if (KHR_materials_variants != null)
            {
                writer.AddProperty("KHR_materials_variants");
                KHR_materials_variants.GltfSerialize(writer);
            }
            writer.Close();
        }
    }

#if DRACO_IS_INSTALLED
    [Serializable]
    public class MeshPrimitiveDracoExtension {
        public int bufferView;
        public Attributes attributes;

        internal void GltfSerialize(JsonWriter writer) {
            writer.AddObject();
            writer.AddProperty("bufferView", bufferView);
            writer.AddProperty("attributes");
            attributes.GltfSerialize(writer);
            writer.Close();
        }
    }
#endif

    /// <summary>
    /// Morph target (blend shape)
    /// </summary>
    [Serializable]
    public class MorphTarget
    {
        // Names are identical to glTF specified property names, that's why
        // inconsistent names are ignored.
        // ReSharper disable InconsistentNaming

        /// <summary>Vertex position deviation accessor index.</summary>
        public int POSITION = -1;
        /// <summary>Vertex normal deviation accessor index.</summary>
        public int NORMAL = -1;
        /// <summary>Vertex tangent deviation accessor index.</summary>
        public int TANGENT = -1;

        // ReSharper restore InconsistentNaming

        /// <summary>
        /// Determines whether two object instances are equal.
        /// </summary>
        /// <param name="obj">The object to compare with the current object.</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        // TODO: Remove upon next major release. This serves no purpose anymore except keeping the API intact.
        public override bool Equals(object obj)
        {
            // ReSharper disable once BaseObjectEqualsIsObjectEquals
            return base.Equals(obj);
        }

        /// <summary>
        /// Default hash function.
        /// </summary>
        /// <returns>A hash code for the current object.</returns>
        // TODO: Remove upon next major release. This serves no purpose anymore except keeping the API intact.
        public override int GetHashCode()
        {
            // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode
            return base.GetHashCode();
        }

        internal void GltfSerialize(JsonWriter writer)
        {
            if (POSITION >= 0) writer.AddProperty("POSITION", POSITION);
            if (NORMAL >= 0) writer.AddProperty("NORMAL", NORMAL);
            if (TANGENT >= 0) writer.AddProperty("TANGENT", TANGENT);
        }
    }
}
